import Cesium, { AssociativeArray } from '../Ces/Cesium'
import WeUniform from './WeUniform'
import WeTextureUniform from './WeTextureUniform'
import WeDisplayShaders from './WeDisplayShaders'
import WeDisplayLegend from './WeDisplayLegend'
import WeDisplayEffectNormal from './WeDisplayEffectNormal'
import WeDisplayValueDecoder from './WeDisplayValueDecoder'
import WeDisplayEffectContour from './WeDisplayEffectContour'
import WeDisplayEffectIcon from './WeDisplayEffectIcon'
import WeImageryLayer from './WeImageryLayer'
import WeCustomTextureUniform from './WeCustomTextureUniform'

const { WeGeneratureFramgmentSource } = WeDisplayShaders

class WeDisplay {
  ffx: number = 1
  dirty: boolean = false
  private debugExtent: boolean = false
  private _loadedTextures: AssociativeArray
  private _weTextureUniform: WeTextureUniform
  private _weUniform: WeUniform
  private _customFragmentShaderSrouce: string
  private _decodeFunc: string
  private _weLegend: WeDisplayLegend
  private _valueDecoder: WeDisplayValueDecoder
  private _normalEffect: WeDisplayEffectNormal
  private _contourEffect: WeDisplayEffectContour
  private _iconEffect: WeDisplayEffectIcon
  private _imageryLayer: WeImageryLayer
  private _cacheShader
  private samplerScratch
  customTextureUniform0: WeCustomTextureUniform
  customTextureUniform1: WeCustomTextureUniform
  constructor(options) {
    if (!options) {
      options = {}
    }
    this._loadedTextures = new Cesium.AssociativeArray()

    const floatUniform = {
      filterMin: -10000000.0,
      filterMax: 10000000.0,
      shouldAnime: 0.0,
      animeStep: 0.0,
      clamp: (options.clamp ?? true) ? 1.0 : 0.0,
      shouldFlipX: (options.flipX ?? false) ? 1.0 : 0.0,
      shouldFlipY: (options.flipY ?? true) ? 1.0 : 0.0,

      //private setting
      paddingEast: 0.0,
    }

    const vec4Uniform = {
      extent: Cesium.Rectangle.MAX_VALUE,
    }

    this._weTextureUniform = new WeTextureUniform({
      ...options,
      display: this
    })
    this._weUniform = new WeUniform({ floatUniform, vec4Uniform })

    this._customFragmentShaderSrouce = options.customFragmentShaderSrouce
    this._decodeFunc = options.decodeFunc
    this.debugExtent = options.debugExtent?? false

    this.ffx = options.ffx?? 1
    this._weLegend = new WeDisplayLegend(options)
    this._valueDecoder = new WeDisplayValueDecoder(options)
    this._normalEffect = new WeDisplayEffectNormal()
    this._contourEffect = new WeDisplayEffectContour(options)
    this._iconEffect = new WeDisplayEffectIcon()
  }

  set imageryLayer(value) {
    this._imageryLayer = value
  }
  get imageryLayer() {
    return this._imageryLayer
  }

  set customFragmentShaderSrouce(value) {
    this._customFragmentShaderSrouce = value
  }

  get weUniform() {
    return this._weUniform
  }
  get normalEffect() {
    return this._normalEffect
  }
  get contourEffect() {
    return this._contourEffect
  }
  get iconEffect() {
    return this._iconEffect
  }
  get valueDecoder() {
    return this._valueDecoder
  }
  get weTextureUniform() {
    return this._weTextureUniform
  }
  get weDisplayLengend() {
    return this._weLegend
  }
  get weDisplayLegend() {
    return this._weLegend
  }
  get valueAndColorRamp() {
    return this._weLegend.valueAndColorRamp
  }
  set valueAndColorRamp(value) {
    this._weLegend.valueAndColorRamp = value
  }
  get filterMin() {
    return this._weUniform.Properties.filterMin
  }
  set filterMin(value) {
    if (this._weUniform.Properties.filterMin !== value) {
      this._weUniform.Properties.filterMin = value
      this.dirty = true
    }
  }
  get filterMax() {
    return this._weUniform.Properties.filterMax
  }
  set filterMax(value) {
    if (this._weUniform.Properties.filterMax !== value) {
      this._weUniform.Properties.filterMax = value
      this.dirty = true
    }
  }
  get clamp() {
    return this._weUniform.Properties.clamp > 0.0
  }
  set clamp(value) {
    if (this._weUniform.Properties.clamp !== value) {
      this._weUniform.Properties.clamp = value
      this.dirty = true
    }
  }
  get shouldAnime() {
    return this._weUniform.Properties.shouldAnime > 0.0
  }
  set shouldAnime(value) {
    if (this._weUniform.Properties.shouldAnime !== value) {
      this._weUniform.Properties.shouldAnime = value
    }
  }
  get animeStep() {
    return this._weUniform.Properties.animeStep
  }
  set animeStep(value) {
    this._weUniform.Properties.animeStep = value
  }
  get flipY() {
    return this._weUniform.Properties.shouldFlipY
  }
  set flipY(value) {
    this._weUniform.Properties.shouldFlipY = value
  }

  getShaderSamplerDeclare() {
    const samplerDeclare1 = this.weTextureUniform.getSamplerDelcare()
    const samplerDeclare2 = this._weLegend.getSamplerDelcare()
    return `
    ${samplerDeclare1}
    ${samplerDeclare2}
  `
  }

  getShaderDefines(toString = false) {
    const defines = []

    if (this._imageryLayer) {
      if (this._imageryLayer.imageryProvider._filterProvider) {
        defines.push(WeDisplayShaders.WeDefineHasFilter)
      }
    }

    this.valueDecoder.pushDefines(defines)
    this._weLegend.pushDefines(defines)
    // this.normalEffect.pushDefines(defines)
    this.contourEffect.pushDefines(defines)
    this.weTextureUniform.pushDefines(defines)
    if (this.debugExtent) {
      defines.push('DEBUG_EXTENT')
    }

    if (toString) {
      let definesStr = ''
      for (let i = 0; i < defines.length; i++) {
        definesStr += `#define ${defines[i]}\n`
      }

      return definesStr
    }

    return defines
  }

  generateDecodeFunc() {
    if (!this._decodeFunc) {
      const { pixelFormat, pixelDatatype } = this.weTextureUniform
      this._decodeFunc = this.valueDecoder.generateDecodeFunc(pixelFormat, pixelDatatype,
        this.weTextureUniform.animation
      )
    }
  }

  getFragmentShaderSource(base = true, toString = false, hasSamplerDeclare = true) {
    let weUniformDeclar = `
      ${this.weUniform.getSource(hasSamplerDeclare)}
      ${this.contourEffect.weUniform.getSource(hasSamplerDeclare)}
      ${this.valueDecoder.weUniform.getSource(hasSamplerDeclare)}
      ${this._decodeFunc}
  `
    const fragmentShaderSource = WeGeneratureFramgmentSource(
      this.getShaderDefines(),
      weUniformDeclar,
      hasSamplerDeclare ? this.getShaderSamplerDeclare() : '',
      base ? ' ' : this.customFragmentShaderSrouce,
    )

    if (toString) {
      let shaderSource = this.getShaderDefines(true)
      fragmentShaderSource.sources.forEach((element) => {
        shaderSource += element
      })

      return shaderSource
    }
    return fragmentShaderSource
  }

  _createTextureWebGL(context, imagery) {
    const imageryLayer = this._imageryLayer
    const imageryProvider = imageryLayer.imageryProvider
    const width = imageryProvider._tileWidth * this.ffx
    const height = imageryProvider._tileHeight * this.ffx

    if (!Cesium.defined(this._cacheShader)) {
      const uniformMap = {}
      this.setupUniformMap(uniformMap)

      const fragmentShaderSource = this.getFragmentShaderSource(false)

      const shaderProgram = Cesium.ShaderProgram.fromCache({
        context,
        vertexShaderSource: Cesium._shadersViewportQuadVS,
        fragmentShaderSource,
        attributeLocations: {
          position: 0,
          textureCoordinates: 1,
        },
      })

      this._cacheShader = {
        uniformMap,
        vertexArray: context.getViewportQuadVertexArray(),
        shaderProgram,
        destroy() {
          if (Cesium.defined(this.shaderProgram)) {
            this.shaderProgram.destroy()
          }
        },
      }
    }

    const sampler = new Cesium.Sampler({
      minificationFilter: imageryLayer.minificationFilter,
      magnificationFilter: imageryLayer.magnificationFilter,
    })

    const outputTexture = new Cesium.Texture({
      context,
      width,
      height,
      pixelFormat: Cesium.PixelFormat.RGBA,
      sampler,
    })

    const { x, y, level } = imagery
    const cacheKey = JSON.stringify([x, y, level])

    // console.log(cacheKey)

    this._loadedTextures.set(cacheKey, outputTexture)

    if (imagery.image instanceof HTMLCanvasElement || imagery.image instanceof HTMLImageElement) {
      outputTexture.filterTexture = new Cesium.Texture({
        context,
        source: imagery.image,
        flipY: false,
      })
    }

    const destroy = outputTexture.destroy
    outputTexture.destroy = () => {
      if (outputTexture.filterTexture) {
        outputTexture.filterTexture.destroy()
      }
      destroy.bind(outputTexture)()
    }

    return outputTexture
  }

  createComputeCommand(_imageryLayer, outputTexture, imagery, _context) {
    const uniformMap = this._cacheShader.uniformMap

    const { west, south, east, north } = imagery.rectangle

    const command = new Cesium.ComputeCommand({
      outputTexture,
      uniformMap,
      persists: true,
    })

    const localExtent = new Cesium.Cartesian4(west, south, east, north)

    command.shaderProgram = this._cacheShader.shaderProgram
    command.preExecute = () => {
      uniformMap.u_localExtent = () => {
        return localExtent
      }

      uniformMap.u_filterTexture = () => {
        return outputTexture.filterTexture || outputTexture._context.defaultTexture
      }

      uniformMap.u_hasFilterTexture = () => {
        return Boolean(outputTexture.filterTexture)
      }
    }
    command.postExecute2 = () => { }

    outputTexture.command = command
    outputTexture.imagery = imagery

    return command
  }

  updateData(context, image1, image2) {
    const imageryLayer = this._imageryLayer

    if (!Cesium.defined(this.samplerScratch)) {
      let wrapS
      if (image1.east && image1.east - image1.west === 360.0) {
        wrapS = Cesium.TextureWrap.REPEAT
      }
      this.samplerScratch = new Cesium.Sampler({
        minificationFilter: imageryLayer.minificationFilter,
        magnificationFilter: imageryLayer.magnificationFilter,
        wrapS
      })
    }

    this._weTextureUniform.updateData(context, image1, image2, {
      sampler: this.samplerScratch,
    })
    this.shouldAnime = Cesium.defined(this._weTextureUniform._lastBaseTexture)
  }

  fixMaterials(material) {
    const uniforms = material._uniforms
    const keys = Object.keys(uniforms)
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i]
      if (k.startsWith('u_BaseTexture')) {
        uniforms[k] = () => this.weTextureUniform.baseTexture
      }
      if (k.startsWith('u_LastBaseTexture')) {
        uniforms[k] = () => this.weTextureUniform.lastBaseTexture
      } else if (k.startsWith('u_TFTexture')) {
        uniforms[k] = () => this._weLegend.texture
      } else if (this.iconEffect.valid && k.startsWith('u_IconTexture')) {
        uniforms[k] = () => this.iconEffect.texture
      }
    }
  }

  setupUniform(uniforms) {
    this.weTextureUniform.setupUniform(uniforms)
    this.weUniform.setupUniform(uniforms)
    this.contourEffect.weUniform.setupUniform(uniforms)
    this.valueDecoder.weUniform.setupUniform(uniforms)
    this.weDisplayLengend.setupUniform(uniforms)
  }

  setupUniformMap(uniformMap) {
    this._weUniform.setupUniformMap(uniformMap)
    this.contourEffect.weUniform.setupUniformMap(uniformMap)
    this.valueDecoder.weUniform.setupUniformMap(uniformMap)
    this._weTextureUniform.setupUniformMap(uniformMap)
    this.weDisplayLengend.setupUniformMap(uniformMap)
  }

  update(frameState) {
    this._weLegend.update(frameState)
    this.weTextureUniform.update(frameState)
    if (!this._imageryLayer || !this.weTextureUniform.ready) return
    this._imageryLayer.ready = true
    if (this.shouldAnime) {
      this.dirty = true
    }

    if (this.dirty) {
      this.dirty = false
      for (let i = 0; i < this._loadedTextures.length; i++) {
        const texture = this._loadedTextures.values[i]

        if (!texture.isDestroyed() && texture.command && texture.imagery) {
          texture.imagery.addReference()
          frameState.commandList.push(texture.command)
        }
      }
    }
  }

  destroy() {
    this.customTextureUniform0 = this.customTextureUniform0?.destroy()
    this.customTextureUniform1 = this.customTextureUniform1?.destroy()
    this._cacheShader = this._cacheShader?.destroy()
    this._weTextureUniform = this._weTextureUniform && this._weTextureUniform.destroy()
    this._weLegend = this._weLegend && this._weLegend.destroy()
    this._iconEffect = this._iconEffect && this._iconEffect.destroy()
    return Cesium.destroyObject(this)
  }

  isDestroyed() {
    return false
  }
}

export default WeDisplay
